using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;

namespace Comms.Drt {
    public class Explorer : IDisposable {
        public class DnsCache {
            public Dictionary<string, IPAddress[]> Cache = new();

            public IPAddress[] Query(string host) {
                lock (Cache) {
                    Cache.TryGetValue(host, out IPAddress[] value);
                    return value;
                }
            }

            public void Add(string host, IPAddress[] addresses) {
                lock (Cache) {
                    Cache[host] = addresses;
                }
            }

            public void Clear() {
                lock (Cache) {
                    Cache.Clear();
                }
            }
        }

        public bool IsDisposed;

        public int[] ServerPorts;

        public bool LocalBroadcast;

        public string[] InternetHosts;

        public double LocalLastTime;

        public double InternetLastTime;

        public Dictionary<IPEndPoint, double> InternetRequestTimes = new();

        public Alarm Alarm;

        public DnsCache Cache = new();

        public List<ServerDescription> ServersList = [];

        public IReadOnlyList<ServerDescription> ServersReadonlyList;

        public MessageSerializer MessageSerializer;

        public int GameTypeID => MessageSerializer.GameTypeID;

        public Peer Peer { get; }

        public IPEndPoint Address => Peer.Address;

        public ExplorerSettings Settings { get; } = new();

        public bool IsDiscoveryStarted => Alarm != null;

        public IReadOnlyList<ServerDescription> DiscoveredServers {
            get {
                lock (Peer.Lock) {
                    CheckNotDisposed();
                    if (ServersReadonlyList == null) {
                        ServersReadonlyList = ServersList.ToArray();
                    }
                    return ServersReadonlyList;
                }
            }
        }

        public event Action<ResourceData> ResourceReceived;

        public event Action<Exception> Error;

        public event Action<string> Debug;

        public event Action<ServerDescription> ServerDiscovered;

        public Explorer(int gameTypeID, int serverPort, int localPort = 0) : this(gameTypeID, [serverPort], localPort) { }

        public Explorer(int gameTypeID, IEnumerable<int> serverPorts, int localPort = 0) : this(
            gameTypeID,
            serverPorts,
            new UdpTransmitter(localPort)
        ) { }

        public Explorer(int gameTypeID, int serverPort, ITransmitter transmitter) : this(gameTypeID, [serverPort], transmitter) { }

        public Explorer(int gameTypeID, IEnumerable<int> serverPorts, ITransmitter transmitter) {
            if (transmitter == null) {
                throw new ArgumentNullException("transmitter");
            }
            if (serverPorts.Any(p => p < 0 || p > 65535)) {
                throw new ArgumentOutOfRangeException("serverPorts");
            }
            MessageSerializer = new MessageSerializer(gameTypeID);
            ServerPorts = serverPorts.ToArray();
            Peer = new Peer(transmitter);
            Peer.Error += delegate(Exception e) {
                if (!(e is MalformedMessageException)) {
                    InvokeError(e);
                }
            };
            Peer.PeerDiscoveryRequest += delegate { };
            Peer.PeerDiscovered += delegate(Packet p) {
                if (!IsDisposed) {
                    Message message = MessageSerializer.Read(p.Bytes, p.Address);
                    if (!(message is ServerDiscoveryResponseMessage message2)) {
                        if (!(message is ServerResourceMessage message3)) {
                            throw new ProtocolViolationException($"Unexpected message type {message.GetType()}.");
                        }
                        Handle(message3, p.Address);
                    }
                    else {
                        Handle(message2, p.Address);
                    }
                }
            };
            Peer.Start();
        }

        public void Dispose() {
            lock (Peer.Lock) {
                if (IsDisposed) {
                    return;
                }
                IsDisposed = true;
            }
            if (Alarm != null) {
                Alarm.Dispose();
                Alarm = null;
            }
            Peer.Dispose();
        }

        public void StartDiscovery(bool localBroadcast = true, IEnumerable<string> internetHosts = null) {
            lock (Peer.Lock) {
                CheckNotDisposed();
                LocalBroadcast = localBroadcast;
                InternetHosts = internetHosts != null ? internetHosts.ToArray() : [];
                LocalLastTime = double.MinValue;
                InternetLastTime = double.MinValue;
                Cache.Clear();
                if (Alarm != null) {
                    Alarm.Dispose();
                }
                Alarm = new Alarm(AlarmFunction);
                Alarm.Error += delegate(Exception e) { InvokeError(e); };
                Alarm.Set(0.0);
            }
        }

        public void StopDiscovery() {
            lock (Peer.Lock) {
                CheckNotDisposed();
                ServersList.Clear();
                ServersReadonlyList = null;
                if (Alarm != null) {
                    Alarm.Dispose();
                    Alarm = null;
                }
            }
        }

        public void RequestResource(IPEndPoint serverAddress, string name, int minimumVersion) {
            lock (Peer.Lock) {
                CheckNotDisposed();
                Peer.DiscoverPeer(
                    serverAddress,
                    MessageSerializer.Write(new ClientResourceRequestMessage { Name = name, MinimumVersion = minimumVersion })
                );
            }
        }

        public void RequestResource(string host, int port, string name, int minimumVersion) {
            Task.Run(
                delegate {
                    IPAddress[] array = DnsQueryHost(host);
                    foreach (IPAddress address in array) {
                        RequestResource(new IPEndPoint(address, port), name, minimumVersion);
                    }
                }
            );
        }

        public void AlarmFunction() {
            lock (Peer.Lock) {
                if (!IsDisposed) {
                    double time = Comm.GetTime();
                    if (time >= LocalLastTime + Settings.LocalDiscoveryPeriod) {
                        LocalLastTime = time;
                        DiscoverLocalServers();
                    }
                    if (time >= InternetLastTime + Settings.InternetDiscoveryPeriod) {
                        InternetLastTime = time;
                        DiscoverInternetServers(InternetHosts);
                    }
                    double val = LocalBroadcast ? Settings.LocalDiscoveryPeriod - (time - LocalLastTime) : double.MaxValue;
                    double val2 = InternetHosts.Count() > 0 ? Settings.InternetDiscoveryPeriod - (time - InternetLastTime) : double.MaxValue;
                    double waitTime = Math.Min(val, val2);
                    if (ServersList.RemoveAll(
                            delegate(ServerDescription s) {
                                double num = Math.Max(
                                    (s.IsLocal ? Settings.LocalRemoveTime : Settings.InternetRemoveTime) - (time - s.DiscoveryTime),
                                    0.0
                                );
                                waitTime = Math.Min(waitTime, num);
                                return num <= 0.0;
                            }
                        )
                        > 0) {
                        ServersReadonlyList = null;
                    }
                    Alarm.Set(waitTime);
                }
            }
        }

        public void DiscoverLocalServers() {
            if (LocalBroadcast) {
                int[] serverPorts = ServerPorts;
                foreach (int peerPort in serverPorts) {
                    Peer.DiscoverLocalPeers(peerPort, MessageSerializer.Write(new ClientDiscoveryRequestMessage()));
                }
            }
        }

        public void DiscoverInternetServers(IEnumerable<string> hosts) {
            foreach (string host in hosts) {
                Task.Run(
                    delegate {
                        IPAddress[] array = DnsQueryHost(host);
                        if (array != null) {
                            lock (Peer.Lock) {
                                IPAddress[] array2 = array;
                                foreach (IPAddress address in array2) {
                                    int[] serverPorts = ServerPorts;
                                    foreach (int port in serverPorts) {
                                        IPEndPoint iPEndPoint = new(address, port);
                                        InternetRequestTimes[iPEndPoint] = Comm.GetTime();
                                        Peer.DiscoverPeer(iPEndPoint, MessageSerializer.Write(new ClientDiscoveryRequestMessage()));
                                    }
                                }
                            }
                        }
                    }
                );
            }
        }

        public void CheckNotDisposed() {
            if (Peer == null) {
                throw new ObjectDisposedException("Server");
            }
        }

        public IPAddress[] DnsQueryHost(string host) {
            IPAddress[] array = Cache.Query(host);
            if (array == null) {
                try {
                    array = Dns.GetHostEntry(host)
                        .AddressList.Where(a => a.AddressFamily == AddressFamily.InterNetwork || a.AddressFamily == AddressFamily.InterNetworkV6)
                        .ToArray();
                    Cache.Add(host, array);
                }
                catch {
                    // ignored
                }
            }
            return array ?? [];
        }

        public void Handle(ServerDiscoveryResponseMessage message, IPEndPoint address) {
            if (!IsDisposed
                && Alarm != null) {
                double time = Comm.GetTime();
                bool isLocal;
                float ping;
                if (InternetRequestTimes.TryGetValue(address, out double value)) {
                    isLocal = false;
                    ping = (float)(time - value);
                }
                else {
                    isLocal = true;
                    ping = (float)(time - LocalLastTime);
                }
                ServersList.RemoveAll(s => Equals(s.Address, address));
                ServerDescription serverDescription = new() {
                    Address = address,
                    Name = message.Name,
                    Priority = message.Priority,
                    IsLocal = isLocal,
                    Ping = ping,
                    DiscoveryTime = time,
                    GameDescriptions = message.GamesDescriptions
                };
                GameDescription[] gameDescriptions = serverDescription.GameDescriptions;
                for (int i = 0; i < gameDescriptions.Length; i++) {
                    gameDescriptions[i].ServerDescription = serverDescription;
                }
                ServersList.Add(serverDescription);
                ServersReadonlyList = null;
                ServerDiscovered?.Invoke(serverDescription);
            }
        }

        public void Handle(ServerResourceMessage message, IPEndPoint address) {
            if (!IsDisposed) {
                ResourceReceived?.Invoke(new ResourceData { Name = message.Name, Version = message.Version, Bytes = message.Bytes });
            }
        }

        public void InvokeError(Exception error) {
            Error?.Invoke(error);
        }

        [Conditional("DEBUG")]
        public void InvokeDebug(string format, params object[] args) {
            if (Debug != null) {
                Debug?.Invoke(string.Format(format, args));
            }
        }
    }
}